#!/usr/bin/env python
# coding=utf-8

import sys
import socket
import re

from os import remove
from os.path import join
from os.path import getsize

import time

import struct
from select import select

import logging

import sqlite3


def decode(string):
    """ decode string with utf if possible """
    try:
        if type(string) == str:
            return string.decode("utf8", "replace")
        else:
            return string
    except:
        return string

if sys.getfilesystemencoding().startswith('ANSI'):
    def fs_encode(string):
        if type(string) == unicode:
            return string.encode('utf8')
        return string

    fs_decode = decode #decode utf8

else:
    fs_encode = fs_decode = lambda x: x  # do nothing


def save_join(*args):
    """ joins a path, encoding aware """
    return fs_encode(join(*[x if type(x) == unicode else decode(x) for x in args]))


class XDCCRequest():
    def __init__(self, timeout=30):
        self.timeout = timeout

        self.filesize = 0
        self.recv = 0
        self.speed = 0

        self.abort = False


    def createSocket(self):
        return socket.socket()


    def download(self, ip, port, filename, irc, fileexists, progressNotify=None):
        ircbuffer = ""
        lastUpdate = time.time()
        cumRecvLen = 0

        dccsock = self.createSocket()

        dccsock.settimeout(self.timeout)
        dccsock.connect((ip, port))

        if fileexists:
            self.recv = getsize(filename)
            fh = open(filename, "a+b")
        else:
            fh = open(filename, "wb")
        
        toread = 4096 * 128
        data = bytearray(toread)
        mv_data = memoryview(data)
        # recv loop for dcc socket
        while self.recv < self.filesize:
            if self.abort:
                dccsock.close()
                fh.close()
                remove(filename)
                return

            self._keepAlive(irc, ircbuffer)
            
            while cumRecvLen < toread and self.recv < self.filesize:
                nbytes = dccsock.recv_into(mv_data[cumRecvLen:], toread - cumRecvLen)
                cumRecvLen += nbytes
                self.recv += nbytes
            
            fh.write(mv_data[:cumRecvLen])

            now = time.time()
            timespan = now - lastUpdate
            if timespan > 1:
                self.speed = cumRecvLen / timespan
                cumRecvLen = 0
                lastUpdate = now

                if progressNotify:
                    progressNotify(self.percent)

            # acknowledge data by sending number of received bytes
            dccsock.send(struct.pack('!I', self.recv))

        dccsock.close()
        fh.close()

        return filename

    def _keepAlive(self, sock, readbuffer):
        fdset = select([sock], [], [], 0)
        if sock not in fdset[0]:
            return

        readbuffer += sock.recv(1024)
        temp = readbuffer.split("\n")
        readbuffer = temp.pop()

        for line in temp:
            line = line.rstrip()
            first = line.split()
            if first[0] == "PING":
                sock.send("PONG %s\r\n" % first[1])

    def abortDownloads(self):
        self.abort = True

    @property
    def size(self):
        return self.filesize

    @property
    def arrived(self):
        return self.recv

    @property
    def percent(self):
        if not self.filesize:
            return 0
        return (self.recv * 100) / self.filesize

    def close(self):
        pass


class Xdcc():
    __name__ = "Xdcc"

    def __init__(self, timeout=30):
        self.debug = 2  #0,1,2
        self.timeout = timeout
        self.multiDL = False


    def init_logger(self, level):
        console = logging.StreamHandler(sys.stdout)
        frm = logging.Formatter("%(asctime)s %(levelname)-8s  %(message)s", "%d.%m.%Y %H:%M:%S")
        console.setFormatter(frm)
        self.log = logging.getLogger("log") # setable in config

        self.log.addHandler(console) #if console logging
        self.log.setLevel(level)


    def process(self, fileno, urls):
        # change request type
        self.req = XDCCRequest()

        idx = 0
        filename, dim, server, chan, bot, pack = urls[idx].slipt('\t')
        nick = 'bl4ckt1ger'
        ident = 'bl4ckt1ger'
        real = 'bl4ckt1ger'

        temp = server.split(':')
        ln = len(temp)
        if ln == 2:
            host, port = temp
        elif ln == 1:
            host, port = temp[0], 6667
        else:
            self.log.error("Invalid hostname for IRC Server (%s)" % server)
            return

        dl_time = time.time()

        sock = socket.socket()
        sock.connect((host, int(port)))
        nick += "-%d" % (time.time() % 1000) # last 3 digits
        sock.send("NICK %s\r\n" % nick)
        sock.send("USER %s %s bla :%s\r\n" % (ident, host, real))

        conn = sqlite3.connect(filename + '.dat')

        with conn:
            cur = conn.cursor()
            while True:
                try:
                    nmn = self.doDownload(filename, fileno, host, port, chan, bot, pack, nick, ident, real)
                except socket.error, e:
                    self.log.error("Failed due to socket errors: %s" % e)

                cur.execute('SELECT Busy FROM FileSplit WHERE Busy = 0')

                if cur.rowcount > 0 and idx < len(urls) - 1:
                    idx += 1
                    filename, dim, server, chan, bot, pack = urls[idx].slipt('\t')
                else:
                    return


    def doDownload(self, filename, fileno, sock, chan, bot, pack, nick):
        #######################
        # CONNECT TO IRC AND IDLE FOR REAL LINK

        # IRC recv loop
        done = False
        retry = None
        m = None

        readbuffer = sock.recv(1024)
        temp = readbuffer.split("\n")
        readbuffer = temp.pop()

        for line in temp:
            if self.debug is 2:
                print "*> " + unicode(line, errors='ignore')
            line = line.rstrip()
            first = line.split()

            if first[0] == "PING":
                sock.send("PONG %s\r\n" % first[1])

        sock.send("JOIN #%s\r\n" % chan)
        sock.send("PRIVMSG %s :xdcc send #%s\r\n" % (bot, pack))

        while True:
            # done is set if we got our real link
            if done:
                break

            if retry:
                if time.time() > retry:
                    retry = None
                    dl_time = time.time()
                    sock.send("PRIVMSG %s :xdcc send #%s\r\n" % (bot, pack))

            else:
                if (dl_time + self.timeout) < time.time(): # todo: add in config
                    sock.send("QUIT :byebye\r\n")
                    sock.close()
                    self.log.error("XDCC Bot did not answer")
                    return

            fdset = select([sock], [], [], 0)
            if sock not in fdset[0]:
                continue

            readbuffer += sock.recv(1024)
            temp = readbuffer.split("\n")
            readbuffer = temp.pop()

            for line in temp:
                if self.debug is 2:
                    print "*> " + unicode(line, errors='ignore')
                line = line.rstrip()
                first = line.split()

                if first[0] == "PING":
                    sock.send("PONG %s\r\n" % first[1])

                elif first[0] == "ERROR":
                    self.log.error("IRC-Error: %s" % line)
                    return

                msg = line.split(None, 3)
                if len(msg) != 4:
                    continue

                msg = {\
                    "origin": msg[0][1:],\
                    "action": msg[1],\
                    "target": msg[2],\
                    "text": msg[3][1:]\
                }

                if nick == msg["target"][0:len(nick)] and "PRIVMSG" == msg["action"]:
                    if msg["text"] == "\x01VERSION\x01":
                        self.log.debug("XDCC: Sending CTCP VERSION.")
                        sock.send("NOTICE %s :%s\r\n" % (msg['origin'], "\x01VERSION mIRC v6.31 Khaled Mardam-Bey\x01"))
                    elif msg["text"] == "\x01TIME\x01":
                        self.log.debug("Sending CTCP TIME.")
                        sock.send("NOTICE %s :%d\r\n" % (msg['origin'], time.time()))
                    elif msg["text"] == "\x01LAG\x01":
                        pass # don't know how to answer

                if not (bot == msg["origin"][0:len(bot)]
                        and nick == msg["target"][0:len(nick)]
                        and msg["action"] in ("PRIVMSG", "NOTICE")):
                    continue

                if self.debug is 1:
                    print "%s: %s" % (msg["origin"], msg["text"])

                if "Hai già richiesto questo pack" in msg["text"]:
                    retry = time.time() + 300

                if "you must be on a known channel to request a pack" in msg["text"]:
                    self.log.error("Wrong channel")
                    return

                m = re.match('\x01DCC SEND (.*?) (\d+) (\d+)(?: (\d+))?\x01', msg["text"])
                if m:
                    # get connection data
                    ip = socket.inet_ntoa(struct.pack('L', socket.ntohl(int(m.group(2)))))
                    port = int(m.group(3))
                    packname = m.group(1)
                    if len(m.groups()) > 3:
                        self.req.filesize = int(m.group(4))
                    if fileexists:
                        sock.send(
                            "PRIVMSG %s :\x01DCC RESUME %s %d %d\x01\r\n" % (bot, packname, port, resumesize))
                    else:
                        done = True
                elif fileexists:
                    m = re.match('\x01DCC ACCEPT (.*?) (\d+)(?: (\d+))?\x01', msg["text"])
                    if m:
                        done = True

        if not fileexists or resumesize < self.req.filesize and resumesize == int(m.group(3)) and port == int(m.group(2)) and packname == m.group(1):
            self.log.info("XDCC: Downloading %s from %s:%d" % (packname, ip, port))

            newname = self.req.download(ip, port, filename, sock, fileexists)
            if newname and newname != filename:
                self.log.info("%(name)s saved as %(newname)s" % {"name": filename, "newname": newname})
                filename = newname

            # kill IRC socket
            # sock.send("QUIT :byebye\r\n")
            sock.close()

            self.lastDownload = filename
            return self.lastDownload


if __name__ == "__main__":
    xdcc = Xdcc()
    xdcc.init_logger(logging.DEBUG)
    xdcc.process('CCleaner.3.27.1900.ITA-BestSoft.rar', 'xdcc://irc.uragano.org/#SunShiNe/SUN|SOFTWARE|02/#125')
    #xdcc.process('Ashampoo.Snap.6.0.4.ITA-BestSoft.rar', 'xdcc://irc.uragano.org/#SunShiNe/SUN|SOFTWARE|02/#126')
